● Zion Boggan
repos/Docket/app/states/[code]/page.tsx
zionboggan.com ↗
179 lines · tsx
History for this file →
1
import type { Metadata } from "next";
2
import Link from "next/link";
3
import { notFound } from "next/navigation";
4
import { Card, PageHeader, SectionHeader } from "@/components/ui";
5
import { PartyTag } from "@/components/party-tag";
6
import { MemberAvatar } from "@/components/member-avatar";
7
import { StatusPill, classifyStatus } from "@/components/status-pill";
8
import { getStateDetail } from "@/lib/feed";
9
import { formatDate } from "@/lib/format";
10
 
11
export const revalidate = 300;
12
 
13
export function generateMetadata({
14
  params,
15
}: {
16
  params: { code: string };
17
}): Metadata {
18
  const detail = getStateDetail(params.code);
19
  if (!detail) return { title: "State not found" };
20
  return { title: `${detail.name} (${detail.code})` };
21
}
22
 
23
function confidenceColor(c: "ok" | "stale" | "missing"): string {
24
  return c === "ok" ? "var(--dk-yea)" : c === "stale" ? "var(--dk-hold)" : "var(--dk-ash)";
25
}
26
 
27
function confidenceLabel(c: "ok" | "stale" | "missing"): string {
28
  return c === "ok" ? "Current" : c === "stale" ? "Stale" : "Coverage rolling out";
29
}
30
 
31
function formatMemberDistrict(chamber: string, district: number | null): string {
32
  if (chamber === "senate") return "";
33
  if (district === null || district <= 0) return " · At-Large";
34
  return ` · Dist. ${district}`;
35
}
36
 
37
export default function StateDetailPage({
38
  params,
39
}: {
40
  params: { code: string };
41
}) {
42
  const detail = getStateDetail(params.code);
43
  if (!detail) notFound();
44
 
45
  return (
46
    <div
47
      className="mx-auto w-full max-w-[860px] px-6 sm:px-8"
48
      style={{
49
        paddingTop: "clamp(32px, 6vw, 56px)",
50
        paddingBottom: "clamp(48px, 8vw, 80px)",
51
      }}
52
    >
53
      <Link
54
        href="/states"
55
        className="focus-ring tap no-underline hover:underline"
56
        style={{
57
          display: "inline-flex",
58
          alignItems: "center",
59
          gap: "8px",
60
          fontFamily: "var(--font-mono), ui-monospace, monospace",
61
          fontSize: "13px",
62
          letterSpacing: "0.08em",
63
          color: "var(--dk-signal)",
64
          marginBottom: "32px",
65
        }}
66
      >
67
        ← States
68
      </Link>
69
 
70
      <PageHeader
71
        eyebrow={`State · ${detail.code}`}
72
        title={detail.name}
73
        lead={`Congressional delegation and sponsored bills for ${detail.name}. State-level legislative coverage is shown honestly below.`}
74
      />
75
 
76
      {}
77
      <div className="mt-8">
78
        <Card className="p-5">
79
          <div className="flex flex-wrap items-center gap-3">
80
            <span
81
              className="inline-flex items-center gap-1.5"
82
              style={{ color: confidenceColor(detail.confidence) }}
83
            >
84
              <span
85
                aria-hidden="true"
86
                className="inline-block h-2 w-2 rounded-full"
87
                style={{ backgroundColor: confidenceColor(detail.confidence) }}
88
              />
89
              <span className="label">
90
                {confidenceLabel(detail.confidence)}
91
              </span>
92
            </span>
93
            <span className="label text-ash-2">
94
              {detail.lastScrape ? `Last seen ${formatDate(detail.lastScrape)}` : "No state-leg scrape on record"}
95
            </span>
96
          </div>
97
          {detail.missingTypes.length > 0 ? (
98
            <p className="mt-3 font-mono text-[13px] text-ash">
99
              State-legislative data not in the record: {detail.missingTypes.join(", ")}. Federal delegation and bill sponsorships below are from congress.gov.
100
            </p>
101
          ) : null}
102
        </Card>
103
      </div>
104
 
105
      <div className="mt-10 grid gap-8 lg:grid-cols-2">
106
        {}
107
        <div>
108
          <SectionHeader
109
            eyebrow={`Current delegation · ${detail.members.length}`}
110
            title="Members of Congress"
111
          />
112
          {detail.members.length === 0 ? (
113
            <Card className="p-5">
114
              <p className="font-mono text-[13px] text-ash-2">
115
                No current members of Congress linked to {detail.name} in the record.
116
              </p>
117
            </Card>
118
          ) : (
119
            <Card className="divide-y divide-[color:var(--dk-line)]">
120
              {detail.members.map((m) => (
121
                <Link
122
                  key={m.id}
123
                  href={m.href}
124
                  className="focus-ring group flex items-center gap-3 p-4 transition-colors hover:bg-graphite"
125
                >
126
                  <MemberAvatar name={m.name} photoUrl={m.photoUrl} size="sm" />
127
                  <span className="min-w-0 flex-1">
128
                    <span className="block font-mono text-[13px] text-paper group-hover:underline">
129
                      {m.name}
130
                    </span>
131
                    <span className="label mt-1 block text-ash-2">
132
                      {m.chamber === "senate" ? "Senate" : "House"}
133
                      {formatMemberDistrict(m.chamber, m.district)}
134
                    </span>
135
                  </span>
136
                  <PartyTag party={m.partyCode} />
137
                </Link>
138
              ))}
139
            </Card>
140
          )}
141
        </div>
142
 
143
        {}
144
        <div>
145
          <SectionHeader
146
            eyebrow={`Bills · ${detail.bills.length}`}
147
            title="Sponsored by delegation"
148
          />
149
          {detail.bills.length === 0 ? (
150
            <Card className="p-5">
151
              <p className="font-mono text-[13px] text-ash-2">
152
                No bills sponsored by the {detail.name} delegation are in the record.
153
              </p>
154
            </Card>
155
          ) : (
156
            <Card className="divide-y divide-[color:var(--dk-line)]">
157
              {detail.bills.slice(0, 20).map((b) => (
158
                <div key={b.id} className="p-4">
159
                  <Link
160
                    href={b.href}
161
                    className="focus-ring font-mono text-[13px] text-paper underline underline-offset-4 hover:text-paper"
162
                  >
163
                    {b.number}
164
                  </Link>
165
                  <p className="mt-1 font-mono text-[12px] text-ash">{b.title}</p>
166
                  <div className="mt-2">
167
                    <StatusPill variant={classifyStatus(b.status)}>
168
                      {b.policyArea || "Introduced"}
169
                    </StatusPill>
170
                  </div>
171
                </div>
172
              ))}
173
            </Card>
174
          )}
175
        </div>
176
      </div>
177
    </div>
178
  );
179
}